﻿using System;
using System.Collections.Specialized;
using System.Net.Mail;
using System.Text;
using System.IO;
using System.Net.Mime;
using System.Collections.Generic;
using System.Diagnostics;

namespace IECAS.ERS.Services.Pop3
{
    public class Pop3TextConverter : IDisposable
    {
        private Int32 _currentMessageLineIndex = 0;
        private List<String> _messageLines;
        private MailMessage _mailMessage = new MailMessage();

        public Pop3TextConverter(String uidl, String entireMessage)
        {
            _messageLines = new List<string>(entireMessage.Split(new String[] { "\r\n" }, StringSplitOptions.None));
            // Extracts the key value pairs from the header inforamtion of the message
            NameValueCollection nvc = ExtractHeaders();
            _mailMessage.Headers.Add(nvc);
            String boundary = GetHeaderAttributeValue(nvc, "content-type", "boundary=").Insert(0, "--");
            RecurseMessageParts(boundary, nvc);
        }

        #region Public Properties
        public MailMessage Message
        {
            get
            {
                return _mailMessage;
            }
        }

        public AttachmentCollection Attachments
        {
            get
            {
                return _mailMessage.Attachments;
            }
        }
        #endregion

        /// <summary>
        /// Will recurse all message parts that are nested within each other using the boundary
        /// as markers.
        /// </summary>
        /// <param name="boundary">Boundary</param>
        /// <param name="nvc">A collection of headers</param>
        private void RecurseMessageParts(String boundary, NameValueCollection nvc)
        {

            Debug.WriteLine("Boundary found " + boundary);

            if (boundary.Equals("--"))
            {
                GetMessagePart(".", nvc);
            }
            else
            {

                String endBoundary = boundary + "--";

                while (!_messageLines[_currentMessageLineIndex].Equals(endBoundary))
                {

                    if (_messageLines[_currentMessageLineIndex].Equals(boundary))
                    {

                        _currentMessageLineIndex++;

                        NameValueCollection nvc2 = ExtractHeaders();
                        String boundary2 = GetHeaderAttributeValue(nvc2, "content-type", "boundary=").Insert(0, "--");

                        if (boundary2.Equals("--"))
                        {
                            GetMessagePart(boundary, nvc2);
                        }
                        else
                        {
                            RecurseMessageParts(boundary2, nvc2);
                        }
                    }
                    else
                    {
                        _currentMessageLineIndex++;
                    }
                }
            }
        }

        /// <summary>
        /// Determines what action to take based on the header values being passed in.
        /// </summary>
        /// <param name="boundary">Boundary</param>
        /// <param name="nvc">A collection of headers</param>
        private void GetMessagePart(String boundary, NameValueCollection nvc)
        {

            String contentDispositionType = GetHeaderStartingValue(nvc, "content-disposition");

            String contentId = GetHeaderStartingValue(nvc, "content-id")
                .Replace("<", String.Empty)
                .Replace(">", String.Empty);

            ContentType contentType = new ContentType(GetHeaderAttributeValue(nvc, "content-type", null));

            String transEnc = GetHeaderStartingValue(nvc, "content-transfer-encoding");

            // Is this an attachment?
            if (contentDispositionType.Equals("attachment"))
            {

                GetAttachment(boundary, nvc, contentType, transEnc, contentDispositionType);

                // Is this a linked resource?
            }
            else if (!String.IsNullOrEmpty(contentId))
            {

                GetLinkedResource(boundary, nvc, contentId, contentType, transEnc);

                // Is this an alternate view?
            }
            else
            {

                GetAlternateView(boundary, nvc, contentType, transEnc);
            }
        }

        private void GetLinkedResource(String boundary, NameValueCollection nvc, String contentId,
                                       ContentType contentType, String transEnc)
        {

            Byte[] content = GetPartAsBytes(boundary, transEnc);

            LinkedResource lr = new LinkedResource(
                new MemoryStream(content));

            lr.ContentId = contentId;
            lr.ContentType = contentType;
            lr.TransferEncoding = GetTransferEncoding(transEnc);

            // Find the alternate view of type html and add the linked resources
            foreach (AlternateView aview in _mailMessage.AlternateViews)
            {
                if (aview.ContentType.MediaType.Equals("text/html"))
                {
                    aview.LinkedResources.Add(lr);
                }
            }
        }

        /// <summary>
        /// Gets an Alternate View and attaches it to the internal MailMessage object.
        /// </summary>
        /// <param name="boundary">Boundary</param>
        /// <param name="nvc">A collection of headers</param>
        /// <param name="contentType">Content type</param>
        /// <param name="transEnc">Transfer encoding used</param>
        private void GetAlternateView(String boundary, NameValueCollection nvc, ContentType contentType, String transEnc)
        {

            String partContent = GetPartAsText(boundary, transEnc);

            if (boundary.Length > 2)
                contentType.Boundary = boundary.Substring(2);

            AlternateView av = AlternateView.CreateAlternateViewFromString(partContent.ToString(), contentType);

            switch (contentType.MediaType)
            {

                case "text/plain":
                    av.ContentType.MediaType = MediaTypeNames.Text.Plain;
                    break;

                case "text/html":
                    av.ContentType.MediaType = MediaTypeNames.Text.Html;
                    _mailMessage.IsBodyHtml = true;
                    break;

                default:
                    throw new Pop3Exception(String.Format("Unknown content type '{0}' for alternate view", contentType));
            }

            av.TransferEncoding = GetTransferEncoding(transEnc);

            _mailMessage.AlternateViews.Add(av);
        }

        private TransferEncoding GetTransferEncoding(String transEnc)
        {

            switch (transEnc)
            {

                case "7bit":
                    return TransferEncoding.SevenBit;

                case "base64":
                    return TransferEncoding.Base64;

                case "quoted-printable":
                    return TransferEncoding.QuotedPrintable;

                default:
                    return TransferEncoding.Unknown;
            }
        }

        /// <summary>
        /// Gets an attachment and adds it to the AttachmentCollection of the internal
        /// MailMessage object.
        /// </summary>
        /// <param name="boundary">Boundary</param>
        /// <param name="nvc">A collection of headers</param>
        /// <param name="contentType">Content type of attachment</param>
        /// <param name="transEnc">Transfer encoding used</param>
        /// <param name="contentDispositionType">Content disposition</param>
        private void GetAttachment(String boundary, NameValueCollection nvc,
                                    ContentType contentType, String transEnc, String contentDispositionType)
        {

            String contentDispositionFilename = GetHeaderAttributeValue(nvc, "content-disposition", "filename=");

            String contentId = GetHeaderStartingValue(nvc, "x-attachment-id");

            contentType.Boundary = boundary.Substring(2);

            MemoryStream ms = new MemoryStream(
                GetPartAsBytes(boundary, transEnc)
            );
            System.Net.Mail.Attachment a = new System.Net.Mail.Attachment(ms, contentType);
            a.ContentDisposition.DispositionType = contentDispositionType;
            a.ContentDisposition.FileName = contentDispositionFilename;
            a.ContentId = contentId;

            _mailMessage.Attachments.Add(a);
        }

        /// <summary>
        /// Returns the text between the internal pointer and the specified boundary and decodes it
        /// using the specified encoding type.
        /// </summary>
        /// <param name="boundary">Message boundary.</param>
        /// <returns>Message as Text.</returns>
        private String GetPartAsText(String boundary, String encoding)
        {

            String part = GetPartAsString(boundary);

            if (encoding.Equals("base64"))
            {
                Byte[] buffer = Convert.FromBase64String(part);
                return Encoding.UTF8.GetString(buffer);
            }

            return part;
        }

        /// <summary>
        /// Returns the text between the internal pointer and the specified boundary as a 
        /// series of decoded bytes using the specified encoding type.
        /// </summary>
        /// <param name="boundary">Boundary</param>
        /// <param name="encoding">Encoding type</param>
        /// <returns>A decoded byte array.</returns>
        private Byte[] GetPartAsBytes(String boundary, String encoding)
        {

            String part = GetPartAsString(boundary);

            if (encoding.ToLower().Equals("base64"))
                return Convert.FromBase64String(part);
            else
                throw new Pop3Exception(String.Format("Unknown encoding type '{0}'", encoding));
        }

        /// <summary>
        /// Retrieves text up until the specified boundary is encountered.
        /// </summary>
        /// <param name="boundary">The boundry to match.</param>
        /// <returns>The text between the current internal pointer location to the boundary.</returns>
        private String GetPartAsString(String boundary)
        {

            StringBuilder sb = new StringBuilder();
            String endBoundary = boundary + "--";

            for (Int32 i = _currentMessageLineIndex; i < _messageLines.Count; i++)
            {

                if (!boundary.Equals(_messageLines[i]) && !endBoundary.Equals(_messageLines[i]))
                {

                    if (_messageLines[i].Equals(String.Empty))
                        sb.AppendLine(_messageLines[i]);
                    else
                        sb.Append(_messageLines[i]);

                }
                else
                {
                    _currentMessageLineIndex = i;
                    break;
                }
            }

            return sb.ToString();
        }

        /// <summary>
        /// Gets the first value from a named message header, usually an unnammed attribute 
        /// value like 'text/plain' or 'attachment'. A semi colon is used to find the boundary.
        /// If no semi colon exists, it returns the entire header value.
        /// </summary>
        /// <param name="searchCollection">NameValueCollection to search.</param>
        /// <returns>The first encountered header value.</returns>
        private String GetHeaderStartingValue(NameValueCollection searchCollection, String headerKey)
        {

            for (Int32 i = 0; i < searchCollection.Count; i++)
            {

                if (searchCollection.GetKey(i).Equals(headerKey, StringComparison.OrdinalIgnoreCase))
                {

                    Int32 _f = searchCollection.Get(i).IndexOf(";");

                    if (_f == -1)
                    {
                        Debug.WriteLine("GetHeaderStartingValue() returned " + searchCollection.Get(i).Substring(0).Trim());
                        return searchCollection.Get(i).Substring(0).Trim();
                    }

                    Debug.WriteLine("GetHeaderStartingValue() returned " + searchCollection.Get(i).Substring(0, _f).Trim());
                    return searchCollection.Get(i).Substring(0, _f).Trim();
                }
            }

            Debug.WriteLine("GetHeaderStartingValue() returned ''");
            return String.Empty;
        }

        /// <summary>
        /// Extracts 'attributes' from header values. Example, "Content-Type: multipart/alternative; boundary=001636457858cec8d104683077d1"
        /// where the attribute value to retrieve would be boundary.
        /// </summary>
        /// <param name="headerKey">The MailMessage header key. In this example, Content-Type.</param>
        /// <param name="valueToRetrieve">The attribute name. In this example, boundary. </param>
        /// <returns>Attribute value.</returns>
        private String GetHeaderAttributeValue(NameValueCollection searchCollection, String headerKey, String attributeKey)
        {
            String returnValue = String.Empty;
            String headerValue;
            for (Int32 i = 0; i < searchCollection.Count; i++)
            {
                if (searchCollection.GetKey(i).Equals(headerKey, StringComparison.OrdinalIgnoreCase))
                {

                    if (attributeKey == null || attributeKey == String.Empty)
                    {

                        Debug.WriteLine("GetHeaderAttributeValue() returned " + searchCollection.Get(i));
                        return searchCollection.Get(i);
                    }
                    headerValue = searchCollection.Get(i);
                    Int32 boundaryPos = headerValue.IndexOf(attributeKey, StringComparison.OrdinalIgnoreCase);
                    // Return empty string value if nothing was found
                    if (boundaryPos == -1) return String.Empty;
                    Int32 boundaryPosEnd = headerValue.IndexOf(";", boundaryPos, StringComparison.OrdinalIgnoreCase);
                    if (boundaryPos != -1)
                    {
                        if (boundaryPosEnd < boundaryPos)
                        {
                            returnValue = headerValue.Substring(boundaryPos + attributeKey.Length);
                        }
                        else
                        {
                            returnValue = headerValue.Substring(boundaryPos + attributeKey.Length, boundaryPosEnd - boundaryPos);
                        }
                        returnValue = returnValue.Replace("\"", String.Empty);
                        break;
                    }
                }
            }
            Debug.WriteLine("GetHeaderAttributeValue() returned " + returnValue);
            return returnValue;
        }

        /// <summary>
        /// Extracts header information from a plain text POP3 reply. Assumes the internal pointer 
        /// _currentMessageLineIndex is at the correct position.
        /// </summary>
        /// <returns>Key/value pairs.</returns>
        private NameValueCollection ExtractHeaders()
        {

            NameValueCollection headers = new NameValueCollection();

            Int32 foundColon, foundSpace = 0;
            String lastHeaderEntry = String.Empty;
            for (Int32 i = _currentMessageLineIndex; i < _messageLines.Count; i++)
            {
                String line = _messageLines[i];
                // Reached end of header information
                if (line.Equals(String.Empty) || line.Equals("."))
                {
                    _currentMessageLineIndex = i;
                    break;
                }
                if (line[0].Equals('+')) continue;
                // From my observation, the only way to tell a multi line header is to check for a space appearing after a colon
                foundColon = line.IndexOf(':');
                foundSpace = line.IndexOf(' ');
                if ((foundColon + 1 == foundSpace) && foundColon != -1)
                {
                    // Remember header for next itteration of for loop
                    lastHeaderEntry = line.Substring(0, foundColon);
                    if (headers.Get(lastHeaderEntry) != null)
                    {
                        // Append multi line header
                        headers[lastHeaderEntry] = headers[lastHeaderEntry] + " " + line.Substring(foundSpace + 1).Trim();
                    }
                    else
                    {
                        String headerValue = line.Substring(foundSpace + 1).Trim();
                        if (!String.IsNullOrEmpty(headerValue))
                        {
                            headers.Add(
                                lastHeaderEntry,
                                line.Substring(foundSpace + 1).Trim()
                            );
                        }
                    }
                }
                else
                {
                    // Append multi line header
                    headers[lastHeaderEntry] = headers[lastHeaderEntry] + " " + line.Trim();
                }
            }
#if DEBUG
            for (int i = 0; i < headers.Count; i++)
            {
                Debug.WriteLine(String.Format("{0}:{1}", headers.GetKey(i), headers.Get(i)));
            }
#endif
            return headers;
        }

        #region IDisposable Members

        public void Dispose()
        {
            _mailMessage.Dispose();
            _mailMessage = null;
            _messageLines.Clear();
            _messageLines = null;
        }

        #endregion
    }
}